﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using YasharEl.Infrastructure.Configuration;
using YasharEl.Infrastructure.Diagnostics.Logging;
using YasharEl.Infrastructure.EntityModel;
using YasharEl.Infrastructure.EntityModel.Specifications;
using YasharEl.Infrastructure.ObjectFactory;
using YasharEl.Infrastructure.Reflection;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Extensibility;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Reporting;
using System.Diagnostics.Contracts;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Execution;
using YasharEl.Infrastructure.ExceptionHandling;
using System.Collections.Specialized;
using YasharEl.Infrastructure.ExceptionHandling.Configuration;
using System.Web;
using System.IO;
using System.Text;
using YasharEl.Infrastructure.ApplicationModel.Bootstrap.Extensions;
using YasharEl.Infrastructure.Diagnostics.Configuration;

namespace YasharEl.Infrastructure.ApplicationModel.Bootstrap
{
    public abstract class BaseBootstrapper<TExtension> : IBootstrapper<TExtension>
        where TExtension : IBootstrappingExtension
    {
        #region Fields

        private readonly IEnumerable<string> defaultAssemblyExclusions = new[] 
        { 
            "system.", 
            "nhibernate.", 
            "log4net.",
            "hibernatingrhinos.",
            "fluentnhibernate.",
            "microsoft.",
            "oracle.",
            "Anaf"
        };

        private readonly IEnumerable<string> defaultFolderExclusions = new[] 
        { 
            "CodeContracts",
            "Seifim"
        };

        private readonly IEnumerable<string> defaultTypeExclusions = new[] 
        { 
            "raven.", 
            "system.", 
            "lucene.", 
            "magnum." 
        };

        private readonly IBootstrappingExtensionHost<TExtension> _extensionHost;
        private readonly IBootstrappingProcessReporter _reporter;

        private IBootstrappingStrategy<TExtension> _strategy;
        private IReportingContext _reportingContext;
        private ILogger<BaseBootstrapper<TExtension>> _bootstrapperLogger = null;

        private readonly DelayedLogger<TExtension> _delayedLogger = new DelayedLogger<TExtension>();

        #endregion

        #region Public Properties

        public BaseDependencyContainer Container { get; protected set; }

        public ILoggerFactory LoggerFactory { get; protected set; }

        public bool IsBootstrapperSuccess
        {
            get;
            private set;
        }

        #endregion

        #region Protected Properties

        protected ILogger<BaseBootstrapper<TExtension>> Logger
        {
            get
            {
                if (_bootstrapperLogger != null)
                    return _bootstrapperLogger;

                _bootstrapperLogger = Container.DependencyResolver.Resolve<ILogger<BaseBootstrapper<TExtension>>>();
                if (_bootstrapperLogger == null)
                {
                    _bootstrapperLogger = new BootstarpperLogger<TExtension>();
                }
                return _bootstrapperLogger;
            }
            private set
            {
                if (value == null)
                {
                    _bootstrapperLogger = new BootstarpperLogger<TExtension>();
                }
                else
                {
                    if (value.GetType() != typeof(DelayedLogger<TExtension>))
                    {
                        DelayedLogger<TExtension> delayedLoggerReference =
                            _bootstrapperLogger as DelayedLogger<TExtension>;
                        if (delayedLoggerReference != null)
                        {
                            _delayedLogger.FlushDelayedLoggineEvents(value);
                        }
                    }

                    _bootstrapperLogger = value;
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is disposed.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
        /// </value>
        protected bool IsDisposed { get; private set; }

        /// <summary>
        /// Returns types in assemblies found in the current directory.
        /// </summary>
        protected IEnumerable<Type> TypesToScan { get; private set; }

        #endregion

        #region Constructors

        public BaseBootstrapper()
            : this(new FifoBasedExtensionHost<TExtension>(), new NullIBootstrappingProcessReporter())
        {

        }

        public BaseBootstrapper(IBootstrappingProcessReporter reporter)
            : this(new FifoBasedExtensionHost<TExtension>(), reporter)
        {
        }

        public BaseBootstrapper(IBootstrappingExtensionHost<TExtension> extensionHost)
            : this(extensionHost, new NullIBootstrappingProcessReporter())
        {
        }

        public BaseBootstrapper(IBootstrappingExtensionHost<TExtension> extensionHost, IBootstrappingProcessReporter reporter)
        {
            Contract.Requires<ArgumentNullException>(extensionHost != null, "extensionHost could not be null.");
            Contract.Requires<ArgumentNullException>(reporter != null, "reporter could not be null.");

            _extensionHost = extensionHost;
            _reporter = reporter;

            Logger = _delayedLogger;
        }

        #endregion

        #region IBootstrapper<TExtension> Implementations

        public void Initialize(IBootstrappingStrategy<TExtension> strategy)
        {
            try
            {
                if (this._strategy != null)
                {
                    throw new BootstrapperException("Bootstrapper can only be initialized once.");
                }

                Logger.Info("Initialize bootstrapping...");

                _strategy = strategy;
                _reportingContext = _strategy.CreateReportingContext();

                // Scan all types depends on enviroment
                if (HttpContext.Current != null)
                {
                    ScanTypesInWebDirectory();
                }
                else
                {
                    ScanTypes(AppDomain.CurrentDomain.BaseDirectory);
                }

                Logger.Info("Create container...");

                // Create specific implemented container.
                Container = CreateContainer();

                Logger.Info("Container created...");

                Logger.Info("Initialize container...");

                // Initialize container.
                Container.InitializeContainer();

                Logger.Info("Container initialized...");

                Logger.Info("Configure container...");

                // Configure container dependencies using configuration source.
                ConfigureContainer();

                Logger.Info("Container configured...");

                Logger.Info("Configure Logger factory...");

                // Configure loggers factory.
                ConfigureLoggerFactory();

                Logger.Info("Bootstrapper logger resolved...");
            }
            catch (InfrastructureCriticalException e)
            {
                try
                {
                    LogFatalException(e);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(e);
                    Debug.WriteLine(ex);
                }
                IsBootstrapperSuccess = false;
                throw;
            }
        }

        public void Run()
        {
            IsBootstrapperSuccess = false;
            try
            {
                if (this._strategy == null)
                {
                    throw new BootstrapperException("Bootstrapper must be initialized before run or shutdown.");
                }

                ConfigureExtensions();

                RunExtensions();

                Logger.Debug("Bootstrapper perform post startup task(s)...");

                // Perform some tasks after application startup.
                PerformPostStartupTask();

                try
                {
                    Logger.Info("Printing dependency container registrations...");

                    IDependencyRegistrationTraverser containerTraverser = Container.CreateContainerTraverser();
                    containerTraverser.Traverse(
                        (registration) =>
                        {
                            Logger.Info(registration.ToString());
                        }
                    );
                }
                finally
                {
                    Logger.Info("Printing dependency container registrations completed.");
                }

                Logger.Debug("Bootstrapper: post startup tasks finished...");

                IsBootstrapperSuccess = true;

                OnStarted(new EventArgs());
            }
            catch (InfrastructureCriticalException e)
            {
                try
                {
                    LogFatalException(e);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(e);
                    Debug.WriteLine(ex);
                }
                IsBootstrapperSuccess = false;
                throw;
            }
        }

        public void Shutdown()
        {
            Logger.Info("Shutdown application...");

            if (_strategy == null)
            {
                throw new BootstrapperException("Bootstrapper must be initialized before run or shutdown.");
            }

            this.Dispose();
        }

        #endregion

        #region Protected Methods

        protected virtual void ConfigureExtensions()
        {
            if (this._strategy == null)
            {
                throw new BootstrapperException("Bootstrapper must be initialized before run or shutdown.");
            }

            // Create bootstrapping extensions resolver
            var extensionResolver = _strategy.CreateExtensionResolver();

            // And add them(found extensions) to bootstrapper extension point(this)
            extensionResolver.Resolve(this);

            // Build run syntax
            var syntax = _strategy.BuildConfigureSyntax();

            // Create executor for executing Configure
            IExecutor<TExtension> configureExecutor = _strategy.CreateConfigureExecutor();

            // Create new execution context and track it using reporting context
            IExecutionContext configureExecutionContext = _reportingContext
                .CreateConfigureExecutionContext(configureExecutor, Container);

            // Execute syntax on all resolved extensions
            configureExecutor.Execute(syntax, _extensionHost.Extensions, configureExecutionContext);
        }

        protected virtual void RunExtensions()
        {
            // Build run syntax
            var syntax = _strategy.BuildRunSyntax();

            // Create executor for executing Run
            IExecutor<TExtension> runExecutor = _strategy.CreateRunExecutor();
            // Create new execution context and track it using reporting context
            IExecutionContext runExecutionContext = _reportingContext
                .CreateRunExecutionContext(runExecutor, Container);

            // Execute syntax on all resolved extensions
            runExecutor.Execute(syntax, _extensionHost.Extensions, runExecutionContext);
        }

        protected virtual void ConfigureLoggerFactory()
        {
            Type loggerFactoryType = typeof(NullLoggerFactory);
            string settingsFile = String.Empty;

            if (LoggingConfig.Instance != null)
            {
                loggerFactoryType = LoggingConfig.Instance.LoggerFactoryType;
                settingsFile = LoggingConfig.Instance.SettingsFile;
            }

            // Register logger factory visible from container.
            Container.DependencyRegistrar.Register(
                typeof(ILoggerFactory),
                loggerFactoryType,
                true
            );

            LoggerFactory = Container.DependencyResolver.Resolve<ILoggerFactory>();
            LoggerFactory.InitializeLoggingFactory(LoggingConfig.Instance.SettingsFile, null);

            // Open generic type for ILogger<> Contract.
            Type openGenericLoggerType = typeof(ILogger<>);

            FactoryMethod factoryMethod = new FactoryMethod(
                    (genericType, resolver, requestedType, key)
                        =>
                    {
                        // Is generic logger type like ILogger<BaseBootstrapper>

                        if (requestedType.IsGenericType)
                        {
                            // Create ILogger<> possible one generic type parameter.

                            Type genericArgumentType = requestedType.GetGenericArguments()[0];

                            return resolver.Resolve<ILoggerFactory>().Create(genericArgumentType);
                        }
                        else
                        {
                            // Create ILogger concrete implementation.
                            return resolver.Resolve<ILoggerFactory>().Create(typeof(ILoggerFactory));
                        }
                    }
                );

            // Register loggers factory method to container.
            // For each Resolve logger factory method will create new transient logger per type.
            Container.DependencyRegistrar.RegisterFactory(openGenericLoggerType, factoryMethod);

            // Refresh logger...
            try
            {
                Logger = Container.DependencyResolver.Resolve<ILogger<BaseBootstrapper<TExtension>>>();
            }
            catch
            {
                Logger = new BootstarpperLogger<TExtension>();
            }
        }

        protected virtual void LogFatalException(Exception e)
        {
            string logName = "YasharEl.Infrastructure";
            if (!EventLog.SourceExists(logName))
            {
                EventLog.CreateEventSource(logName, logName);
            }

            EventLog eventLog = new EventLog();
            eventLog.Source = logName;
            eventLog.WriteEntry(e.FullMessage(), EventLogEntryType.Error);
        }

        #endregion

        #region Abstract Methods

        protected abstract BaseDependencyContainer CreateContainer();

        protected abstract void ConfigureContainer();

        protected abstract void PerformPostStartupTask();

        #endregion

        #region IExtensionPoint<TExtension> Implementations

        public void AddExtension(TExtension extension)
        {
            if (this._strategy == null)
            {
                throw new BootstrapperException("Bootstrapper must be initialized before run or shutdown.");
            }

            _reportingContext.CreateExtensionContext(extension);

            extension.Container = this.Container;
            extension.Logger = this.Logger;
            extension.HostingBootstrapper = this;
            extension.ScannedTypes = TypesToScan;

            _extensionHost.AddExtension(extension);
        }

        #endregion

        #region IDisposable Implementations

        public void Dispose()
        {
            this.Dispose(true);

            GC.SuppressFinalize(this);
        }

        ~BaseBootstrapper()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.IsDisposed && disposing)
            {
                var syntax = _strategy.BuildShutdownSyntax();

                IExecutor<TExtension> shutdownExecutor = _strategy.CreateShutdownExecutor();
                IExecutionContext shutdownExecutionContext = _reportingContext
                    .CreateShutdownExecutionContext(shutdownExecutor, Container);

                shutdownExecutor.Execute(syntax, _extensionHost.Extensions, shutdownExecutionContext);

                _reporter.Report(_reportingContext);

                _strategy.Dispose();

                IsDisposed = true;
            }
        }

        #endregion

        #region Types Scan Helper Methods

        /// <summary>
        /// Configures bootstrapper to scan for assemblies 
        /// in the relevant web directory instead of regular
        /// runtime directory.
        /// </summary>
        /// <returns></returns>
        private void ScanTypesInWebDirectory()
        {
            ScanTypes(HttpRuntime.BinDirectory);
        }

        /// <summary>
        /// Configures bootstrapper to scan for assemblies
        /// in the given directory rather than the regular
        /// runtime directory.
        /// </summary>
        /// <param name="probeDirectory"></param>
        /// <returns></returns>
        private void ScanTypes(string probeDirectory)
        {
            ScanTypes(GetAssembliesInDirectory(probeDirectory));
        }

        /// <summary>
        /// Configures bootstrapper to use the types found in the given assemblies.
        /// </summary>
        /// <param name="assemblies"></param>
        /// <returns></returns>
        private void ScanTypes(IEnumerable<Assembly> assemblies)
        {
            ScanTypes(assemblies.ToArray());
        }

        /// <summary>
        /// Configures bootstrapper to scan the given assemblies only.
        /// </summary>
        /// <param name="assemblies"></param>
        /// <returns></returns>
        private void ScanTypes(params Assembly[] assemblies)
        {
            var types = new List<Type>();
            Array.ForEach(
                assemblies,
                a =>
                {
                    try
                    {
                        types.AddRange(a.GetTypes()
                            .Where(t => !t.IsValueType &&
                                        (t.FullName == null ||
                                                !defaultTypeExclusions.Any(exclusion => t.FullName.ToLower().StartsWith(exclusion)))));
                    }
                    catch (ReflectionTypeLoadException e)
                    {
                        var sb = new StringBuilder();
                        sb.Append(string.Format("Could not scan assembly: {0}. Exception message {1}.", a.FullName, e));
                        if (e.LoaderExceptions.Any())
                        {
                            sb.Append(Environment.NewLine + "Scanned type errors: ");
                            foreach (var ex in e.LoaderExceptions)
                                sb.Append(Environment.NewLine + ex.Message);
                        }
                        Logger.Warn(sb.ToString());
                        return;//intentionally swallow exception
                    }
                });

            ScanTypes(types);
        }

        /// <summary>
        /// Configures bootstrapper to scan the given types.
        /// </summary>
        /// <param name="typesToScan"></param>
        /// <returns></returns>
        private void ScanTypes(IEnumerable<Type> typesToScan)
        {
            TypesToScan = typesToScan;
            Logger.Debug("Bootstrapper - number of types to scan: {0}", TypesToScan.Count());
        }

        /// <summary>
        /// Load and return all assemblies in the given directory except the given ones to exclude
        /// </summary>
        /// <param name="path"></param>
        /// <param name="assembliesToSkip"></param>
        /// <returns></returns>
        private IEnumerable<Assembly> GetAssembliesInDirectory(string path, params string[] assembliesToSkip)
        {
            foreach (var a in GetAssembliesInDirectoryWithExtension(path, "*.exe", assembliesToSkip))
                yield return a;
            foreach (var a in GetAssembliesInDirectoryWithExtension(path, "*.dll", assembliesToSkip))
                yield return a;
        }

        private IEnumerable<Assembly> GetAssembliesInDirectoryWithExtension(string path, string extension, params string[] assembliesToSkip)
        {
            var result = new List<Assembly>();

            foreach (FileInfo file in new DirectoryInfo(path).GetFiles(extension, SearchOption.TopDirectoryOnly))
            {
                try
                {
                    if (defaultAssemblyExclusions.Any(exclusion => file.Name.ToLower().StartsWith(exclusion)))
                        continue;

                    if (assembliesToSkip.Contains(file.Name, StringComparer.InvariantCultureIgnoreCase))
                        continue;

                    result.Add(Assembly.LoadFrom(file.FullName));
                }
                catch (BadImageFormatException bif)
                {
                    if (bif.FileName.ToLower().Contains("system.data.sqlite.dll"))
                        throw new BadImageFormatException(
                            "You've installed the wrong version of System.Data.SQLite.dll on this machine. If this machine is x86, this dll should be roughly 800KB. If this machine is x64, this dll should be roughly 1MB. You can find the x86 file under /binaries and the x64 version under /binaries/x64. *If you're running the samples, a quick fix would be to copy the file from /binaries/x64 over the file in /binaries - you should 'clean' your solution and rebuild after.",
                            bif.FileName, bif);

                    throw new InvalidSystemConfigurationException(
                        "Could not load " + file.FullName + ". Consider to except (\"" + file.Name + "\"))' to tell bootstrapper not to load this file.",
                        bif
                    );
                }
            }

            foreach (DirectoryInfo directory in new DirectoryInfo(path).GetDirectories())
            {
                if (defaultFolderExclusions.Any(exclusion => directory.Name.ToLower().StartsWith(exclusion.ToLower())))
                    continue;

                result.AddRange(GetAssembliesInDirectoryWithExtension(directory.FullName, extension, assembliesToSkip));
            }

            return result;
        }

        #endregion

        #region IBootstrapper<TExtension> Events

        public event EventHandler Started;

        #endregion

        #region Started Event Fire

        protected virtual void OnStarted(EventArgs e)
        {
            if (Started != null)
            {
                Started(this, e);
            }
        }

        #endregion
    }
}
